“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
- Java Development Kit (JDK) 17+
- Maven or Gradle
- An IDE (IntelliJ IDEA Community / Eclipse / VS Code)
- 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 athttp://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 likesave()
,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 yourBanana
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
- Add More Fields: Extend the
Banana
entity with fields likeoriginCountry
(String) andharvestDate
(LocalDate). Update the DDL-auto settings tocreate-drop
for fresh tables. - Custom Query: Implement a new repository method
List
and expose it as a new GET endpointfindByNameContaining(String name) /api/bananas/search?query=...
. - 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!