Build a RESTful CRUD API in Java with Spring Boot

Build a RESTful CRUD API in Java with Spring Boot

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: