Build a RESTful CRUD API in Java with Spring Boot
Spring Boot has become the go-to framework for building production-ready Java applications — and one of its most popular uses is creating RESTful APIs. In this tutorial, we’ll build a complete CRUD (Create, Read, Update, Delete) API with Spring Boot from scratch. You’ll learn how to design routes, manage data persistence, and even write some unit tests.
1. Project Setup with Spring Initializr
To get started, head over to start.spring.io and create a new Spring Boot project using the following dependencies:
- Spring Web
- Spring Data JPA
- H2 Database
Once generated, import the project into your favorite IDE (like IntelliJ IDEA or Eclipse).
Directory Structure
src/main/java/
└── com.example.demo
├── controller
├── model
├── repository
└── service
This will help separate concerns in our application.
2. Define the Entity
We’ll build a simple “Book” API where each book has an ID, title, author, and published year. Let’s define the model class.
package com.example.demo.model;
import jakarta.persistence.*;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private int year;
// Getters and setters omitted for brevity
}
This entity will map to a table named book
in your embedded H2 database.
3. Repository and Service Layers
Using Spring Data JPA allows us to avoid boilerplate code when interacting with the database.
Repository Interface
package com.example.demo.repository;
import com.example.demo.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {}
Service Layer
package com.example.demo.service;
import com.example.demo.model.Book;
import com.example.demo.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class BookService {
@Autowired
private BookRepository repository;
public List<Book> getAllBooks() {
return repository.findAll();
}
public Optional<Book> getBookById(Long id) {
return repository.findById(id);
}
public Book createBook(Book book) {
return repository.save(book);
}
public Book updateBook(Long id, Book bookDetails) {
Book book = repository.findById(id).orElseThrow();
book.setTitle(bookDetails.getTitle());
book.setAuthor(bookDetails.getAuthor());
book.setYear(bookDetails.getYear());
return repository.save(book);
}
public void deleteBook(Long id) {
repository.deleteById(id);
}
}
This service encapsulates all the data access logic and helps us follow the single responsibility principle.
4. Creating the REST Controller
Now it’s time to expose our service as a RESTful API using HTTP methods.
package com.example.demo.controller;
import com.example.demo.model.Book;
import com.example.demo.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
@Autowired
private BookService bookService;
@GetMapping
public List<Book> getAllBooks() {
return bookService.getAllBooks();
}
@GetMapping("/{id}")
public ResponseEntity<Book> getBookById(@PathVariable Long id) {
return bookService.getBookById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public Book createBook(@RequestBody Book book) {
return bookService.createBook(book);
}
@PutMapping("/{id}")
public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book book) {
try {
return ResponseEntity.ok(bookService.updateBook(id, book));
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
return ResponseEntity.noContent().build();
}
}
This controller handles all CRUD endpoints and maps HTTP verbs to appropriate methods.
5. Testing the API
You can now run your Spring Boot app and test the API using tools like Postman, curl, or by writing unit tests.
Testing with curl
# Create a book
curl -X POST -H "Content-Type: application/json" -d '{"title":"1984", "author":"George Orwell", "year":1949}' http://localhost:8080/api/books
# Get all books
curl http://localhost:8080/api/books
# Get book by ID
curl http://localhost:8080/api/books/1
# Update a book
curl -X PUT -H "Content-Type: application/json" -d '{"title":"Animal Farm", "author":"George Orwell", "year":1945}' http://localhost:8080/api/books/1
# Delete a book
curl -X DELETE http://localhost:8080/api/books/1
Writing Integration Tests
package com.example.demo;
import com.example.demo.model.Book;
import com.example.demo.repository.BookRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class BookApiTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private BookRepository bookRepository;
@Test
void shouldCreateAndRetrieveBook() throws Exception {
String json = "{\"title\":\"Clean Code\",\"author\":\"Robert C. Martin\",\"year\":2008}";
mockMvc.perform(post("/api/books")
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("Clean Code"));
}
}
Writing integration tests ensures your endpoints behave as expected and helps catch regressions earlier.
Final Thoughts & Tips
- Use DTOs (Data Transfer Objects) to separate API models from your database entities in larger applications
- Add validation using annotations like @NotNull, @Size, etc., for cleaner API contracts
- Use pagination when querying large lists – Spring Data JPA supports this out of the box
- Document your API using Swagger/OpenAPI for better developer experience
Spring Boot makes building REST APIs in Java both fast and elegant. With this foundation, you can build complex systems by layering on security, versioning, or asynchronous processing for even better performance.
Useful links: