javabeginner
Spring Boot Fundamentals — REST APIs with Java
Build production REST APIs with Spring Boot — controllers, services, repositories, JPA, validation, security basics, and the patterns senior Java developers use.
LearnixoApril 16, 20265 min read
JavaSpring BootREST APIJPASpring SecurityBeginner
Spring Boot is the most widely used Java framework for building REST APIs. It eliminates boilerplate configuration and gives you a production-ready server in minutes. This lesson covers the full stack from first endpoint to database to security.
Create a Project
Go to start.spring.io and select:
- Project: Maven
- Language: Java
- Spring Boot: 3.x (latest)
- Dependencies: Spring Web, Spring Data JPA, PostgreSQL Driver, Validation, Spring Security
Or use the CLI:
Bash
spring init --dependencies=web,data-jpa,postgresql,validation,security my-api
cd my-api
./mvnw spring-boot:runProject Structure
src/main/java/com/example/api/
├── ApiApplication.java ← Entry point
├── controller/
│ └── ProductController.java
├── service/
│ └── ProductService.java
├── repository/
│ └── ProductRepository.java
├── model/
│ └── Product.java ← JPA entity
├── dto/
│ ├── CreateProductRequest.java
│ └── ProductResponse.java
├── exception/
│ ├── ResourceNotFoundException.java
│ └── GlobalExceptionHandler.java
└── config/
└── SecurityConfig.javaYour First REST Controller
JAVA
package com.example.api.controller;
import com.example.api.dto.CreateProductRequest;
import com.example.api.dto.ProductResponse;
import com.example.api.service.ProductService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping
public Page<ProductResponse> getAll(Pageable pageable) {
return productService.findAll(pageable);
}
@GetMapping("/{id}")
public ProductResponse getById(@PathVariable Long id) {
return productService.findById(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ProductResponse create(@Valid @RequestBody CreateProductRequest request) {
return productService.create(request);
}
@PutMapping("/{id}")
public ProductResponse update(@PathVariable Long id,
@Valid @RequestBody CreateProductRequest request) {
return productService.update(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
productService.delete(id);
}
}JPA Entity
JAVA
package com.example.api.model;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.Instant;
@Entity
@Table(name = "products")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(nullable = false)
private Integer stock = 0;
@Column(nullable = false, length = 100)
private String category;
private Boolean active = true;
@CreationTimestamp
@Column(updatable = false)
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
}Repository — Spring Data JPA
JAVA
package com.example.api.repository;
import com.example.api.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Derived query methods — Spring generates the SQL
Page<Product> findByActiveTrue(Pageable pageable);
Page<Product> findByCategoryAndActiveTrue(String category, Pageable pageable);
Optional<Product> findByIdAndActiveTrue(Long id);
// Custom JPQL query
@Query("SELECT p FROM Product p WHERE p.price BETWEEN :min AND :max AND p.active = true")
Page<Product> findByPriceRange(@Param("min") BigDecimal min,
@Param("max") BigDecimal max,
Pageable pageable);
// Native SQL query
@Query(value = "SELECT * FROM products WHERE to_tsvector('english', name) @@ plainto_tsquery(:term)",
nativeQuery = true)
Page<Product> fullTextSearch(@Param("term") String term, Pageable pageable);
boolean existsByNameIgnoreCase(String name);
}Service Layer
JAVA
package com.example.api.service;
import com.example.api.dto.CreateProductRequest;
import com.example.api.dto.ProductResponse;
import com.example.api.exception.ResourceNotFoundException;
import com.example.api.model.Product;
import com.example.api.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ProductService {
private final ProductRepository productRepository;
public Page<ProductResponse> findAll(Pageable pageable) {
return productRepository.findByActiveTrue(pageable)
.map(ProductResponse::from);
}
public ProductResponse findById(Long id) {
return productRepository.findByIdAndActiveTrue(id)
.map(ProductResponse::from)
.orElseThrow(() -> new ResourceNotFoundException("Product not found: " + id));
}
@Transactional
public ProductResponse create(CreateProductRequest request) {
Product product = Product.builder()
.name(request.name())
.description(request.description())
.price(request.price())
.stock(request.stock())
.category(request.category())
.build();
return ProductResponse.from(productRepository.save(product));
}
@Transactional
public ProductResponse update(Long id, CreateProductRequest request) {
Product product = productRepository.findByIdAndActiveTrue(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found: " + id));
product.setName(request.name());
product.setDescription(request.description());
product.setPrice(request.price());
product.setStock(request.stock());
return ProductResponse.from(productRepository.save(product));
}
@Transactional
public void delete(Long id) {
Product product = productRepository.findByIdAndActiveTrue(id)
.orElseThrow(() -> new ResourceNotFoundException("Product not found: " + id));
product.setActive(false); // soft delete
productRepository.save(product);
}
}DTOs with Validation
JAVA
// Request DTO
public record CreateProductRequest(
@NotBlank(message = "Name is required")
@Size(max = 200, message = "Name must be 200 characters or less")
String name,
String description,
@NotNull(message = "Price is required")
@DecimalMin(value = "0.01", message = "Price must be positive")
@Digits(integer = 8, fraction = 2)
BigDecimal price,
@Min(value = 0, message = "Stock cannot be negative")
Integer stock,
@NotBlank(message = "Category is required")
String category
) {}
// Response DTO
public record ProductResponse(
Long id,
String name,
String description,
BigDecimal price,
Integer stock,
String category,
Instant createdAt
) {
public static ProductResponse from(Product p) {
return new ProductResponse(
p.getId(), p.getName(), p.getDescription(),
p.getPrice(), p.getStock(), p.getCategory(), p.getCreatedAt()
);
}
}Global Exception Handler
JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> errors = ex.getBindingResult()
.getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid"
));
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
pd.setTitle("Validation Failed");
pd.setProperty("errors", errors);
return pd;
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ProblemDetail handleUnexpected(Exception ex) {
log.error("Unexpected error", ex);
return ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"An unexpected error occurred"
);
}
}application.properties
PROPERTIES
# Database
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
# JPA / Hibernate
spring.jpa.hibernate.ddl-auto=validate # use Flyway/Liquibase in production
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=true
# Pagination defaults
spring.data.web.pageable.default-page-size=20
spring.data.web.pageable.max-page-size=100Quick Reference
JAVA
@RestController // Returns JSON by default
@RequestMapping("/api") // Base URL prefix
@GetMapping / @PostMapping / @PutMapping / @DeleteMapping
@PathVariable Long id // /products/{id}
@RequestParam int page // /products?page=1
@RequestBody @Valid // Parse and validate JSON body
@ResponseStatus(CREATED) // Override default 200
Repository:
JpaRepository<Entity, Id> // CRUD + pagination built-in
Derived methods: findByField() / findByFieldAndOtherField()
JPQL: @Query("SELECT e FROM Entity e WHERE ...")
Pagination: Page<T> findAll(Pageable pageable)
Service:
@Service
@Transactional(readOnly = true) // default for read methods
@Transactional // for write methods
Exception:
@RestControllerAdvice + @ExceptionHandler
ProblemDetail (Spring 6+ RFC 9457 format)REST API Knowledge Check
5 questions · Test what you just learned · Instant explanations
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.