“Hooking up a database to your Spring Boot app is like giving your monkey a permanent memory — it’ll remember all the bananas it has collected!” 🍌💾

You’ve mastered creating basic REST endpoints. Now, let’s give your application the power to remember things! We’ll build a simple API that stores and retrieves data using Spring Data JPA and the easy-to-use H2 in-memory database. By the end, you’ll have a fully functional, database-backed REST service.


🚩 Prerequisites

  1. Java Development Kit (JDK) 17+
  2. Maven or Gradle
  3. An IDE (IntelliJ IDEA Community / Eclipse / VS Code)
  4. A basic Spring Boot project (you can use one from How to set up your first Spring Boot project).

1️⃣ Add Dependencies

Open your pom.xml (Maven) or build.gradle (Gradle) and add the following:

Maven (pom.xml)


  org.springframework.boot
  spring-boot-starter-data-jpa
  
  
  com.h2database
  h2
  runtime
  
  
  
  org.springframework.boot
  spring-boot-starter-web
  
  

Gradle (build.gradle)

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  runtimeOnly 'com.h2database:h2'
  // Optional: for web features like REST controllers
  implementation 'org.springframework.boot:spring-boot-starter-web'
  }
  

2️⃣ Configure H2 Database

Add these lines to your src/main/resources/application.properties:

spring.h2.console.enabled=true
  spring.h2.console.path=/h2-console
  spring.datasource.url=jdbc:h2:mem:testdb
  spring.datasource.driverClassName=org.h2.Driver
  spring.datasource.username=sa
  spring.datasource.password=password
  spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
  spring.jpa.hibernate.ddl-auto=update # Creates/updates schema automatically
  
  • ddl-auto=update: Hibernate will automatically create or update your database schema based on your entities. Great for development!
  • h2.console.enabled: This gives you a web-based UI to inspect your in-memory database at http://localhost:8080/h2-console (default port).

3️⃣ Create Your Entity (The Banana!)

An Entity is a plain Java object that represents a table in your database. Let’s create a Banana entity:

// src/main/java/com/example/demo/model/Banana.java
  package com.example.demo.model;
  
  
  import jakarta.persistence.Entity;
  import jakarta.persistence.GeneratedValue;
  import jakarta.persistence.GenerationType;
  import jakarta.persistence.Id;
  import jakarta.validation.constraints.Min;
  import jakarta.validation.constraints.NotBlank;
  import jakarta.validation.constraints.NotNull;
  
  
  @Entity // Marks this class as a JPA entity
  public class Banana {
  
  
  @Id // Primary key
  @GeneratedValue(strategy = GenerationType.IDENTITY) // Auto-increment ID
  private Long id;
  
  @NotBlank(message = "Banana name cannot be empty")
  private String name;
  
  @NotNull(message = "Banana ripeness cannot be null")
  @Min(value = 1, message = "Ripeness must be at least 1 (green)")
  private Integer ripeness; // 1 (green) to 5 (overripe)
  
  // JPA requires a no-arg constructor
  public Banana() {}
  
  public Banana(String name, Integer ripeness) {
      this.name = name;
      this.ripeness = ripeness;
  }
  
  // --- Getters and Setters ---
  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 Integer getRipeness() { return ripeness; }
  public void setRipeness(Integer ripeness) { this.ripeness = ripeness; }
  
  @Override
  public String toString() {
      return "Banana{id=" + id + ", name='" + name + "', ripeness=" + ripeness + '}';
  }
  
  }
  

4️⃣ Create Your Repository (The Banana Finder!)

Spring Data JPA makes interacting with your database incredibly easy. Just define an interface, and Spring generates the implementation:

// src/main/java/com/example/demo/repository/BananaRepository.java
package com.example.demo.repository;

import com.example.demo.model.Banana;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository // Marks this as a Spring Data repository
public interface BananaRepository extends JpaRepository {
    // JpaRepository provides CRUD (Create, Read, Update, Delete) methods

    // Custom query method - Spring Data derives SQL from the method name
    List findByRipenessGreaterThanEqual(Integer minRipeness);
}
  • JpaRepository: Inherits methods like save(), findById(), findAll(), delete().
  • findByRipenessGreaterThanEqual(): Spring Data automatically creates the SQL query for this! ✨

5️⃣ Create Your Service Layer (The Banana Manager!)

Keep your business logic out of the controllers. A service class handles operations:

// src/main/java/com/example/demo/service/BananaService.java
  package com.example.demo.service;
  
  
  import com.example.demo.model.Banana;
  import com.example.demo.repository.BananaRepository;
  import org.springframework.stereotype.Service;
  import java.util.List;
  import java.util.Optional;
  
  
  @Service // Marks this as a Spring service component
  public class BananaService {
  
  
  private final BananaRepository bananaRepository;
  
  // Dependency Injection: Spring automatically provides BananaRepository
  public BananaService(BananaRepository bananaRepository) {
      this.bananaRepository = bananaRepository;
  }
  
  public List getAllBananas() {
      return bananaRepository.findAll();
  }
  
  public Optional getBananaById(Long id) {
      return bananaRepository.findById(id);
  }
  
  public Banana saveBanana(Banana banana) {
      return bananaRepository.save(banana);
  }
  
  public void deleteBanana(Long id) {
      bananaRepository.deleteById(id);
  }
  
  public List getRipeBananas(Integer minRipeness) {
      return bananaRepository.findByRipenessGreaterThanEqual(minRipeness);
  }
  
  }
  

6️⃣ Create Your Controller (The Banana API!)

Now, let’s expose these operations via a REST API:

// src/main/java/com/example/demo/controller/BananaController.java
  package com.example.demo.controller;
  
  
  import com.example.demo.model.Banana;
  import com.example.demo.service.BananaService;
  import jakarta.validation.Valid;
  import org.springframework.http.HttpStatus;
  import org.springframework.http.ResponseEntity;
  import org.springframework.web.bind.annotation.*;
  
  
  import java.util.List;
  
  
  @RestController
  @RequestMapping("/api/bananas") // Base path for all banana endpoints
  public class BananaController {
  
  
  private final BananaService bananaService;
  
  public BananaController(BananaService bananaService) {
      this.bananaService = bananaService;
  }
  
  @GetMapping
  public List getAllBananas() {
      return bananaService.getAllBananas();
  }
  
  @GetMapping("/{id}")
  public ResponseEntity getBananaById(@PathVariable Long id) {
      return bananaService.getBananaById(id)
              .map(ResponseEntity::ok) // If found, return 200 OK with banana
              .orElse(ResponseEntity.notFound().build()); // Else, return 404 Not Found
  }
  
  @PostMapping
  @ResponseStatus(HttpStatus.CREATED) // Return 201 Created
  public Banana createBanana(@Valid @RequestBody Banana banana) {
      return bananaService.saveBanana(banana);
  }
  
  @PutMapping("/{id}")
  public ResponseEntity updateBanana(@PathVariable Long id, @Valid @RequestBody Banana bananaDetails) {
      return bananaService.getBananaById(id)
              .map(existingBanana -> {
                  existingBanana.setName(bananaDetails.getName());
                  existingBanana.setRipeness(bananaDetails.getRipeness());
                  return ResponseEntity.ok(bananaService.saveBanana(existingBanana));
              })
              .orElse(ResponseEntity.notFound().build());
  }
  
  @DeleteMapping("/{id}")
  @ResponseStatus(HttpStatus.NO_CONTENT) // Return 204 No Content
  public void deleteBanana(@PathVariable Long id) {
      bananaService.deleteBanana(id);
  }
  
  @GetMapping("/ripe")
  public List getRipeBananas(@RequestParam(defaultValue = "3") Integer minRipeness) {
      return bananaService.getRipeBananas(minRipeness);
  }
  
  }
  
  • @Valid: Triggers the validation annotations (like @NotBlank, @Min) on your Banana entity. If invalid, Spring returns a 400 Bad Request.
  • ResponseEntity: Allows fine-grained control over HTTP status codes (200 OK, 201 Created, 404 Not Found, etc.).

7️⃣ Run Your Application

Start your Spring Boot application (e.g., from your IDE or via ./mvnw spring-boot:run / ./gradlew bootRun).

You should see logs indicating H2 database initialization. Now, let’s test it!


8️⃣ Test Your API with cURL

1. Access H2 Console

Go to http://localhost:8080/h2-console in your browser. Use jdbc:h2:mem:testdb for the JDBC URL, sa for username, and password for password. Click “Connect”. You should see your BANANA table!

2. Create a Banana (POST)

curl -X POST -H "Content-Type: application/json" 
  
  -d '{"name": "Cavendish", "ripeness": 3}' 
  
  http://localhost:8080/api/bananas
  
  
  Expected: Returns the created banana with an ID, e.g., {"id":1,"name":"Cavendish","ripeness":3}
  
  

3. Get All Bananas (GET)

curl http://localhost:8080/api/bananas
  
  
  Expected: [ {"id":1,"name":"Cavendish","ripeness":3} ]
  
  

4. Get a Banana by ID (GET /{id})

curl http://localhost:8080/api/bananas/1
  
  
  Expected: {"id":1,"name":"Cavendish","ripeness":3}
  
  

5. Update a Banana (PUT /{id})

curl -X PUT -H "Content-Type: application/json" 
  
  -d '{"name": "Cavendish", "ripeness": 4}' 
  
  http://localhost:8080/api/bananas/1
  
  
  Expected: {"id":1,"name":"Cavendish","ripeness":4}
  
  

6. Find Ripe Bananas (GET /ripe)

curl http://localhost:8080/api/bananas/ripe?minRipeness=4
  
  
  Expected: [ {"id":1,"name":"Cavendish","ripeness":4} ]
  
  

7. Delete a Banana (DELETE /{id})

curl -X DELETE http://localhost:8080/api/bananas/1
  
  
  Expected: No content (204)
  
  

💡 Monkey-Proof Tips

  • DTOs vs. Entities: For real-world apps, consider creating separate DTOs (Data Transfer Objects) for your API requests/responses to avoid exposing internal entity details.
  • Error Handling: Implement a global @ControllerAdvice to catch exceptions (e.g., ConstraintViolationException, NoSuchElementException) and return consistent JSON error messages.
  • Real Databases: To use a persistent database (PostgreSQL, MySQL, etc.), replace the H2 dependency with your database driver and update application.properties with its connection details.
  • Testing: Write integration tests using @SpringBootTest and a test database (e.g., Testcontainers) to ensure your API and database interactions work as expected.

🚀 Challenge

  1. Add More Fields: Extend the Banana entity with fields like originCountry (String) and harvestDate (LocalDate). Update the DDL-auto settings to create-drop for fresh tables.
  2. Custom Query: Implement a new repository method List findByNameContaining(String name) and expose it as a new GET endpoint /api/bananas/search?query=....
  3. Error Handling: Add @NotNull to a field and test creating a banana without that field. Observe the 400 Bad Request error. Now, implement a simple @ControllerAdvice to return a custom error message for validation failures.

👏 You’ve successfully built your first database-backed Spring Boot API! You’re now a certified banana-managing backend champion. Next up: securing your API with Spring Security—keeping those bananas safe from hungry hackers!

By admin

Leave a Reply

Your email address will not be published. Required fields are marked *