Creating a Clean REST API Using Spring Boot

Creating a Clean REST API Using Spring Boot 3.2 and Java 21 (With Full CRUD Example)


In the modern world of microservices and cloud-native applications, building a clean, maintainable, and scalable REST API is more crucial than ever. With Spring Boot 3.2 and Java 21, you can leverage the latest features to write expressive and performance-optimized code. 

Creating a Clean REST API Using Spring Boot 3.2 and Java 21 (With Full CRUD Example)


In this post, you'll learn how to build a clean RESTful API with full CRUD (Create, Read, Update, Delete) operations using Spring Boot 3.2 and Java 21. We'll also focus on best practices and structure our code following clean architecture principles.


️ Tools & Technologies

  • Java 21
  • Spring Boot 3.2
  • Spring Web
  • Spring Data JPA
  • H2 Database (for testing)
  • Lombok (for boilerplate reduction)

✅ Project Setup

Create a Spring Boot project using your favorite IDE or Spring Initializr with the following dependencies:

  • Spring Web
  • Spring Data JPA
  • H2 Database
  • Lombok

pom.xml (Key Dependencies)

<properties>
    <java.version>21</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>


隣 Project Structure

com.example.restapi
├── controller
│   └── ProductController.java
├── dto
│   └── ProductDTO.java
├── entity
│   └── Product.java
├── repository
│   └── ProductRepository.java
├── service
│   └── ProductService.java
└── RestApiApplication.java


 Define the Entity

package com.example.restapi.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Double price;
}


 Create a DTO (Data Transfer Object)

package com.example.restapi.dto;

public record ProductDTO(Long id, String name, Double price) {}

Using record from Java 21 keeps the DTO immutable and concise.


 Repository Layer

package com.example.restapi.repository;

import com.example.restapi.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

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


 Service Layer (Business Logic)

package com.example.restapi.service;

import com.example.restapi.dto.ProductDTO;
import com.example.restapi.entity.Product;
import com.example.restapi.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepository repository;

    public List<ProductDTO> findAll() {
        return repository.findAll()
                .stream()
                .map(p -> new ProductDTO(p.getId(), p.getName(), p.getPrice()))
                .toList();
    }

    public ProductDTO findById(Long id) {
        return repository.findById(id)
                .map(p -> new ProductDTO(p.getId(), p.getName(), p.getPrice()))
                .orElseThrow(() -> new RuntimeException("Product not found"));
    }

    public ProductDTO create(ProductDTO dto) {
        Product saved = repository.save(Product.builder()
                .name(dto.name())
                .price(dto.price())
                .build());
        return new ProductDTO(saved.getId(), saved.getName(), saved.getPrice());
    }

    public ProductDTO update(Long id, ProductDTO dto) {
        Product existing = repository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found"));
        existing.setName(dto.name());
        existing.setPrice(dto.price());
        Product updated = repository.save(existing);
        return new ProductDTO(updated.getId(), updated.getName(), updated.getPrice());
    }

    public void delete(Long id) {
        repository.deleteById(id);
    }
}


 REST Controller

package com.example.restapi.controller;

import com.example.restapi.dto.ProductDTO;
import com.example.restapi.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
    private final ProductService service;

    @GetMapping
    public List<ProductDTO> getAll() {
        return service.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductDTO> getById(@PathVariable Long id) {
        return ResponseEntity.ok(service.findById(id));
    }

    @PostMapping
    public ResponseEntity<ProductDTO> create(@RequestBody ProductDTO dto) {
        return ResponseEntity.ok(service.create(dto));
    }

    @PutMapping("/{id}")
    public ResponseEntity<ProductDTO> update(@PathVariable Long id, @RequestBody ProductDTO dto) {
        return ResponseEntity.ok(service.update(id, dto));
    }

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


⚙️ Application Properties

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update


離 Testing the API

Use tools like Postman or cURL to test the following endpoints:

  • GET /api/products
  • GET /api/products/{id}
  • POST /api/products
  • PUT /api/products/{id}
  • DELETE /api/products/{id}

 SEO & Performance Best Practices

  • Use DTOs to control response structure
  • Use records (Java 21) for immutability and clarity
  • Avoid exposing entities directly
  • Enable pagination and filtering in production-ready APIs
  • Proper exception handling with @ControllerAdvice

 Conclusion

Building a clean REST API with Spring Boot 3.2 and Java 21 is not just easier—it's more powerful than ever. With records, clean separation of concerns, and Spring Boot’s robust ecosystem, you can deliver maintainable and scalable backends with ease.

  • Follow Clean Architecture
  • Use DTOs and Services
  • Write testable, maintainable code

If you found this guide helpful, consider sharing it with your team or bookmarking it for future reference!

Have feedback or questions? Drop them in the comments below!

Post a Comment

Previous Post Next Post