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. Usevalidatein production2spring.jpa.show-sql=true: Displays SQL queries in console for debuggingspring.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();
}
}
@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@PrePersistand@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);
}
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
@Queryannotation for complex queries11 - Native SQL Queries: Set
nativeQuery = truefor 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
@Transactionalfor 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@Controllerand@ResponseBodyfor REST APIs1617@RequestMapping("/api/products"): Base URL for all endpoints18@Valid: Triggers validation on request body98ResponseEntity: 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 Method | Endpoint | Description | Status Code |
|---|---|---|---|
| POST | /api/products | Create new product | 201 Created |
| GET | /api/products | Get all products | 200 OK |
| GET | /api/products/{id} | Get product by ID | 200 OK / 404 Not Found |
| PUT | /api/products/{id} | Update product | 200 OK / 404 Not Found |
| DELETE | /api/products/{id} | Delete product | 204 No Content / 404 Not Found |
| GET | /api/products/available | Get available products | 200 OK |
| GET | /api/products/category/{category} | Get by category | 200 OK |
| GET | /api/products/search?keyword=x | Search by name | 200 OK |
| GET | /api/products/price-range?min=x&max=y | Get by price range | 200 OK |
| GET | /api/products/low-stock?threshold=x | Get low stock products | 200 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
- 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
- Reduces boilerplate code with built-in CRUD methods5
- Automatic query generation from method names11
- Support for custom queries with
@Queryannotation11 - Transaction management with
@Transactional13
- Use Bean Validation annotations (
@NotBlank,@Size, etc.)8 - Enable validation with
@Validin controllers9 - Handle validation errors with
@RestControllerAdvice15
Exception Handling:15
- Create custom exceptions for domain-specific errors15
- Use
@RestControllerAdvicefor global exception handling15 - Return consistent error responses with appropriate HTTP status codes15
- 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
-
https://developer.okta.com/blog/2018/12/13/build-basic-app-spring-boot-jpa ↩
-
https://www.bezkoder.com/spring-boot-postgresql-example/ ↩ ↩2 ↩3
-
https://dev.to/igventurelli/connecting-spring-boot-applications-to-a-database-with-spring-data-jpa-46ch ↩ ↩2
-
https://stackoverflow.com/questions/76418413/add-postgresql-dependency-with-spring-boot ↩
-
https://www.bezkoder.com/spring-boot-jpa-crud-rest-api/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
https://www.linkedin.com/pulse/entity-annotation-spring-boot-venura-pavan-zesde ↩ ↩2 ↩3 ↩4
-
https://www.codingshuttle.com/blog/top-10-jpa-annotations-in-spring-boot ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
https://www.bezkoder.com/spring-boot-validate-request-body/ ↩ ↩2 ↩3 ↩4
-
https://www.geeksforgeeks.org/springboot/request-body-and-parameter-validation-with-spring-boot/ ↩ ↩2 ↩3 ↩4
-
https://dev.to/tienbku/spring-boot-postgresql-maven-crud-example-598m ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
https://stackoverflow.com/questions/57284058/what-is-the-best-practice-for-restcontroller ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
https://pwskills.com/blog/architecture-of-spring-boot-examples-pattern-layered-controller-layer/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10
-
https://stackoverflow.com/questions/58234187/what-is-the-use-of-service-layer-in-spring-boot-applications ↩ ↩2
-
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
-
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
-
https://camunda.com/blog/2025/05/how-to-build-a-rest-api-with-spring-boot-a-step-by-step-guide/ ↩ ↩2 ↩3 ↩4
-
https://dzone.com/articles/build-crud-restful-api-using-spring-boot-3 ↩ ↩2 ↩3 ↩4
-
https://dev.to/devcorner/mastering-responseentity-and-controller-in-spring-boot-agc ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
https://www.geeksforgeeks.org/advance-java/how-to-use-spring-responseentity-to-manipulate-the-http-response/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
https://blog.jetbrains.com/idea/2024/10/how-to-build-a-crud-rest-api-using-spring-boot/ ↩ ↩2
-
https://www.geeksforgeeks.org/advance-java/best-practices-while-making-rest-apis-in-spring-boot-application/ ↩
-
https://github.com/bezkoder/spring-boot-3-rest-api-example ↩
-
https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/configure-spring-data-jpa-with-azure-postgresql ↩
-
https://stackoverflow.com/questions/69408767/can-i-use-spring-data-jpa-with-postgresql ↩
-
https://amigoscode.com/blogs/top-10-spring-boot-rest-api-best-practices ↩
-
https://mkyong.com/spring-boot/spring-boot-spring-data-jpa-postgresql/ ↩
-
https://stackoverflow.com/questions/48307487/setting-up-spring-boot-jpa-postgresql ↩
-
https://www.geeksforgeeks.org/springboot/spring-boot-integration-with-postgresql-as-a-maven-project/ ↩
-
https://stackoverflow.com/questions/60800005/how-to-correctly-configure-spring-data-jpa-into-the-application-properties-confi ↩
-
https://stackoverflow.com/questions/13242196/how-do-you-add-postgresql-driver-as-a-dependency-in-maven ↩
-
https://stackoverflow.com/questions/30310988/how-can-i-load-jpa-properties-to-datasource-in-spring ↩
-
https://thorben-janssen.com/configuring-spring-data-jpa-with-spring-boot/ ↩
-
https://dev.to/wkreuch/create-an-entity-and-repository-using-spring-boot-3-2l6 ↩
-
https://www.geeksforgeeks.org/springboot/spring-boot-application-properties/ ↩
-
https://mvnrepository.com/artifact/org.postgresql/postgresql ↩
-
https://docs.spring.io/spring-boot/appendix/application-properties/index.html ↩
-
https://www.enterprisedb.com/postgres-tutorials/how-add-postgresql-driver-dependency-maven ↩
-
https://www.tutorialspoint.com/spring_boot_orm/spring_boot_orm_application.htm ↩
-
https://stackoverflow.com/questions/43832705/spring-boot-does-entity-annotation-exist ↩
-
https://docs.spring.io/spring-boot/how-to/properties-and-configuration.html ↩
-
https://stackoverflow.com/questions/64517537/springboot-validate-requestbody ↩
-
https://stackoverflow.com/questions/49732262/spring-responseentity-best-practice ↩
-
https://www.geeksforgeeks.org/springboot/spring-boot-architecture/ ↩
-
https://stackoverflow.com/questions/48385362/spring-boot-validation-of-requestbody-dto-annotated-in-rest-api ↩
-
https://www.baeldung.com/jsf-spring-boot-controller-service-dao ↩
-
https://www.linkedin.com/pulse/setting-up-validations-request-body-spring-boot-kāshān-asim-vy8le ↩
-
https://www.reddit.com/r/SpringBoot/comments/1buv6hn/what_are_the_best_practices_in_spring_boot/ ↩
-
https://stackoverflow.com/questions/68264638/correct-pattern-for-handling-service-layer-results ↩
-
https://blog.tericcabrel.com/validate-request-body-and-parameter-in-spring-boot/ ↩
-
https://www.reddit.com/r/SpringBoot/comments/1e8smxf/best_practices_return_value/ ↩
-
https://dev.to/writech/designing-a-multi-layered-architecture-for-building-restful-web-services-with-spring-boot-and-kotlin-51l5 ↩