Build a Bookstore REST API
Build a Spring Boot REST API with full CRUD and an embedded H2 database.
Introduction
30 Second Summary
Every time you browse a bookstore website, add an item to your cart, or search for a title, your browser is talking to a backend API. That API receives your request, looks up data in a database, and sends back a response.
In this project, you will build a REST API for a bookstore using Spring Boot. Your API will create, read, update, and delete books stored in an embedded database.
What You'll Build
A Bookstore REST API powered by Spring Boot 4.0.7 that exposes endpoints to manage a collection of books (title, author, ISBN, price) using an embedded H2 database.
By the end of this project, you'll have:
- A Spring Boot project structured with proper layered architecture: controller, service, and repository.
- A Book entity mapped to an H2 database table using JPA.
- Five RESTful CRUD endpoints tested with curl and returning JSON responses.
- Secret Mission: Add a search endpoint that finds books by author name.
Are there any prerequisites?
No prior experience with Spring Boot or Java web development is required. This guide includes setup for all required tools, including JDK 21 and IntelliJ IDEA.
Set Up Your Development Environment
Before you can build your REST API, you need a working Java development environment and a Spring Boot project to work inside.
In this step, you will install Java, generate a starter project using Spring Initializr, and confirm everything runs. By the end, your server will be live and ready for code.
In this step, get ready to:
- Install and verify JDK 21.
- Generate a Spring Boot project from Spring Initializr.
- Open the project in IntelliJ IDEA and confirm the server starts.
Install JDK 21
Spring Boot 4.x requires Java 17 or later. This project uses JDK 21, a long-term support release that gives you access to the latest language features.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check whether Java is already installed by running this command:
java --version
✔️ I see version 21 or higher
Your output shows something like openjdk 21.0.x. Java is ready to go.
- Confirm the compiler is also available by running:
javac --version
You should see a matching version number. You are all set to move on.
ⓧ I see an older version
Your system has Java installed, but it is below version 21. You need to install JDK 21 alongside it or upgrade.
macOS
- Install JDK 21 using Homebrew by running:
brew install openjdk@21
- After installation, link it so your system can find it by running:
sudo ln -sfn $(brew --prefix)/opt/openjdk@21/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-21.jdk
- Close and reopen your terminal, then verify with:
java --version
You should now see version 21.
Windows
- Install JDK 21 using winget by running:
winget install Microsoft.OpenJDK.21
- Close and reopen PowerShell, then verify with:
java --version
You should now see version 21.
Linux
- On Ubuntu/Debian, install JDK 21 by running:
sudo apt update && sudo apt install openjdk-21-jdk -y
- On Fedora/RHEL, run this instead:
sudo dnf install -y java-21-openjdk-devel
- Close and reopen your terminal, then verify with:
java --version
You should now see version 21.
ⓧ Command not found
Java is not installed on your machine yet. Follow the instructions for your operating system below.
macOS
- Install JDK 21 using Homebrew by running:
brew install openjdk@21
- After installation, link it so your system can find it by running:
sudo ln -sfn $(brew --prefix)/opt/openjdk@21/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-21.jdk
- Close and reopen your terminal, then verify with:
java --version
You should now see version 21.
Windows
- Install JDK 21 using winget by running:
winget install Microsoft.OpenJDK.21
- Close and reopen PowerShell, then verify with:
java --version
You should now see version 21.
Linux
- On Ubuntu/Debian, install JDK 21 by running:
sudo apt update && sudo apt install openjdk-21-jdk -y
- On Fedora/RHEL, run this instead:
sudo dnf install -y java-21-openjdk-devel
- Close and reopen your terminal, then verify with:
java --version
You should now see version 21.
- After confirming java --version shows version 21, also verify the compiler by running:
javac --version
You should see a matching javac 21.x.x output. Both tools are now ready.
What is a JDK?
The JDK (Java Development Kit) includes the Java compiler (javac) and the runtime (java). You need both to write and run Java applications.
Generate the Spring Boot project
Spring Initializr is an official web tool that generates a ready-to-use Spring Boot project structure for you. You select your build tool, language, and dependencies, and it produces a downloadable ZIP.
- Open your browser and go to https://start.spring.io.
- Set Project to Maven.
- Set Language to Java.
- Set Spring Boot to 4.0.7.
- Under Project Metadata, set Group to com.example.
- Set Artifact to bookstore.
- Click Add Dependencies.
- Search for and add Spring Web.
- Search for and add Spring Data JPA.
- Search for and add H2 Database.
- Click Generate to download the ZIP file.
- Extract the downloaded bookstore.zip to your Desktop.
What do these dependencies do?
Spring Web provides the tools to build REST API endpoints and run an embedded web server.
Spring Data JPA lets you save and retrieve Java objects from a database without writing SQL.
H2 Database is an embedded in-memory database that requires zero setup. It lives inside your application and resets every time you restart.
Why Maven over Gradle?
Maven is more common in enterprise Java projects and has simpler, more readable configuration files for beginners. The project includes a Maven Wrapper (mvnw), so you do not need to install Maven separately.
Open the project in IntelliJ IDEA
Now that you have the project files on your Desktop, open them in IntelliJ so you can run and edit the code.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type IntelliJ and press Enter to open it.
- On the Welcome screen, click Open.
- Navigate to your Desktop and select the bookstore folder you just extracted.
- Click OK (or Open) to open the project.
IntelliJ detects the Maven project and begins importing dependencies automatically. Wait for the progress bar in the bottom-right corner to finish. This may take a minute or two on first import.
What is happening during import?
IntelliJ reads the pom.xml file, downloads all the libraries your project depends on (Spring Boot, H2, etc.), and indexes the code so that auto-complete and navigation work correctly.
- Once indexing is complete, open the built-in terminal in IntelliJ by clicking Terminal at the bottom of the window.
- Start the application by running:
./mvnw spring-boot:run
What does this command do?
The ./mvnw script is the Maven Wrapper bundled with your project. It downloads the correct Maven version if needed, then builds and runs your Spring Boot app. No global Maven install required.
On Windows, use mvnw.cmd spring-boot:run instead if you see a "not recognized" error.
- Wait for the console output to show a line containing Started BookstoreApplication. This means the server is running.
- Open your browser and go to http://localhost:8080.
You should see a Whitelabel Error Page. This confirms the server is running but has no endpoints defined yet.
- Return to IntelliJ's terminal and press Ctrl+C to stop the server.
Server did not start?
- If you see a permission error on macOS/Linux, make the wrapper executable first by running chmod +x mvnw in your terminal, then try again.
- If you see port 8080 already in use, another process is occupying that port. Close other servers or IDE run configurations and retry.
- If Maven cannot download dependencies, check your internet connection and ensure no firewall is blocking outbound HTTPS traffic.
Help me start my Spring Boot application.
✔️ Awesome, I've got everything!
Great. Double check you have the server stopped (Ctrl+C in the terminal) before moving on.
ⓧ I'd like to double check the full code
The main application file was auto-generated by Spring Initializr. Open src/main/java/com/example/bookstore/BookstoreApplication.java and confirm it looks like this:
package com.example.bookstore;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BookstoreApplication {
public static void main(String[] args) {
SpringApplication.run(BookstoreApplication.class, args);
}
}
Your development environment is ready. Next up, you will create a Book entity that maps directly to a database table.
Create the Book Entity
Your development environment is ready and Spring Boot starts without errors. Now it's time to define the data your API will manage.
In Spring Boot, you represent database tables as JPA entities. You'll create a Book entity class that maps to an H2 database table, then configure the database so you can inspect it in a browser.
In this step, get ready to:
- Create the Book.java entity class with JPA annotations.
- Configure H2 database console and JPA settings.
- Verify the BOOK table appears in the H2 console.
Create the Book entity class
A JPA entity is a plain Java class annotated with @Entity. Each field becomes a column in the database table. The @Id and @GeneratedValue annotations tell the database to auto-generate a unique ID for each new record.
- In IntelliJ, right-click the src/main/java/com/example/bookstore/ package in the Project sidebar.
- Select New, then Java Class.
- Name it Book and press Enter.
- Replace the entire contents of the new file with the following code:
package com.example.bookstore;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private String isbn;
private double price;
public Book() {
}
public Book(String title, String author, String isbn, double price) {
this.title = title;
this.author = author;
this.isbn = isbn;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
What does this code do?
@Entity tells JPA to map this class to a database table called BOOK (derived from the class name).
@Id marks id as the primary key. @GeneratedValue(strategy = GenerationType.IDENTITY) makes the database auto-increment the ID for each new book.
The remaining fields (title, author, isbn, price) each become a column. JPA requires a no-argument constructor, and the parameterized constructor is a convenience for creating new Book instances.
- Save Book.java.
- Confirm Book.java appears in the com.example.bookstore package in the IntelliJ Project sidebar, alongside BookstoreApplication.java.
Don't see Book.java in the right place?
- Make sure the file is in src/main/java/com/example/bookstore/, not in a nested sub-package or directly under src/.
- Check that your imports use jakarta.persistence, not javax.persistence. Spring Boot 4.x uses the Jakarta namespace.
Why isn't my Book.java file compiling in IntelliJ?
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
package com.example.bookstore;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
private String isbn;
private double price;
public Book() {
}
public Book(String title, String author, String isbn, double price) {
this.title = title;
this.author = author;
this.isbn = isbn;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
Configure H2 and JPA settings
Spring Boot reads configuration from application.properties. You need to enable the H2 web console so you can inspect your database, and tell JPA to log SQL queries for debugging.
- Open src/main/resources/application.properties in IntelliJ.
- Replace its contents with the following configuration:
spring.h2.console.enabled=true
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
What do these properties do?
- spring.h2.console.enabled=true activates a browser-based database console at /h2-console so you can inspect tables and run queries.
- spring.jpa.show-sql=true prints every SQL statement to the terminal, which helps you see exactly what JPA does behind the scenes.
- spring.jpa.hibernate.ddl-auto=create-drop tells Hibernate to create tables on startup and drop them on shutdown. This keeps the database fresh during development.
- Save application.properties.
You won't see a visible change yet. The next substep runs the application to verify both the entity and this configuration.
File appears empty or read-only?
- Make sure you opened the file at src/main/resources/application.properties, not src/test/resources/application.properties.
- If IntelliJ shows the file as read-only, right-click the file tab and select Make Writable.
Why can't I edit application.properties in IntelliJ?
✔️ Awesome, I've got everything!
Great. Make sure you saved the file before continuing.
ⓧ I'd like to double check the full code
spring.h2.console.enabled=true
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop
Verify the BOOK table in H2 console
With the entity and configuration in place, Spring Boot will automatically create a BOOK table when the application starts. You can confirm this using the H2 browser console.
- Start the application by running this command in the IntelliJ terminal:
./mvnw spring-boot:run
Wait for the startup to finish. Look for a log line in your terminal that says something like:
H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:...'
Why does the log matter?
Spring Boot auto-generates the database URL. The log line shows the actual JDBC URL you need to connect to the H2 console. Copy that URL from the log. It will look like jdbc:h2:mem: followed by a unique identifier.
- Open your browser and go to http://localhost:8080/h2-console.
- In the JDBC URL field, paste the URL from the startup log. If the log shows jdbc:h2:mem:testdb, use that. Otherwise, paste whatever URL appeared in the log.
- Leave the User Name as sa and the Password field empty.
- Click Connect.
You should see a BOOK table in the left panel. Click on it to confirm columns matching your entity fields: ID, TITLE, AUTHOR, ISBN, and PRICE.
Can't connect or don't see the BOOK table?
- Double-check the JDBC URL. Copy it directly from the startup log line that mentions the database being available. Do not guess the URL.
- Make sure the application is still running. If you stopped it, the in-memory database is gone. Restart with ./mvnw spring-boot:run and try again.
- Confirm spring.h2.console.enabled=true is in your application.properties file at src/main/resources/.
Why can't I connect to the H2 console or see the BOOK table?
- Stop the server by pressing Ctrl+C in the terminal.
Your data model is defined and the database creates the table automatically. Next up, you'll build the repository and service layer that read and write Book records.
Create the Repository and Service Layer
In the previous step, you created the Book entity and confirmed it maps to a database table automatically.
Now you need a way to actually interact with that table. In Spring Boot, this is handled by two layers: a repository that talks to the database, and a service that contains your business logic. This separation keeps your code organized the way employers expect in production Java projects.
In this step, get ready to:
- Create a repository interface that gives you database operations for free.
- Create a service class with full CRUD business logic.
- Verify the project compiles without errors.
Create the BookRepository interface
Spring Data JPA lets you define a repository interface and get full CRUD database methods without writing any implementation code. Spring generates the implementation at runtime based on the interface you declare.
- In IntelliJ, right-click the src/main/java/com/example/bookstore/ package in the **Project** panel on the left.
- Select New, then Java Class.
- Name it BookRepository and select Interface from the Kind dropdown.
- Replace the contents of the file with the following code:
package com.example.bookstore;
import org.springframework.data.jpa.repository.JpaRepository;
// Extends JpaRepository to get save, findById, findAll, deleteById for free
public interface BookRepository extends JpaRepository<Book, Long> {
}
What does this code do?
By extending JpaRepository<Book, Long>, Spring Data JPA automatically provides methods like save(), findById(), findAll(), and deleteById().
The two type parameters tell Spring which entity this repository manages (Book) and the type of its primary key (Long). You get a fully working data access layer with zero SQL.
- Save BookRepository.java.
- You won't see a visible change yet. The service class in the next substep uses this repository.
Seeing a red underline on JpaRepository?
Make sure IntelliJ has finished importing Maven dependencies. Check the bottom status bar for any progress indicators. If the import is stale, right-click the pom.xml and select Maven then Reload project.
help me fix unresolved imports in my Spring Boot project
✔️ Awesome, I've got everything!
Great. Make sure you have saved BookRepository.java before moving on.
ⓧ I'd like to double check the full code
package com.example.bookstore;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
}
Create the BookService class
The service layer sits between the controller (which receives HTTP requests) and the repository (which talks to the database). It contains your business logic and keeps responsibilities separated cleanly.
- In IntelliJ, right-click the src/main/java/com/example/bookstore/ package again.
- Select New, then Java Class.
- Name it BookService and keep Class selected as the Kind.
- Replace the contents of the file with the following code:
package com.example.bookstore;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
// Marks this class as a Spring-managed service component
@Service
public class BookService {
// Spring injects the BookRepository automatically via constructor injection
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
// Returns all books from the database
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
// Finds a single book by its ID, returns empty if not found
public Optional<Book> getBookById(Long id) {
return bookRepository.findById(id);
}
// Saves a new book to the database
public Book createBook(Book book) {
return bookRepository.save(book);
}
// Updates an existing book's fields, returns empty if the book doesn't exist
public Optional<Book> updateBook(Long id, Book bookDetails) {
return bookRepository.findById(id).map(book -> {
book.setTitle(bookDetails.getTitle());
book.setAuthor(bookDetails.getAuthor());
book.setIsbn(bookDetails.getIsbn());
book.setPrice(bookDetails.getPrice());
return bookRepository.save(book);
});
}
// Deletes a book if it exists, returns true if deleted
public boolean deleteBook(Long id) {
if (bookRepository.existsById(id)) {
bookRepository.deleteById(id);
return true;
}
return false;
}
}
What does this code do?
- @Service tells Spring to manage this class as a bean, making it available for dependency injection elsewhere in the app.
- The constructor accepts a BookRepository parameter. Spring automatically passes in the repository instance at startup. This pattern is called constructor injection.
- getAllBooks() and getBookById() delegate to the repository's built-in query methods.
- updateBook() uses Optional.map() to update fields only if the book exists, then saves the updated book back.
- deleteBook() checks existence first and returns a boolean so the controller can respond with the correct HTTP status.
- Save BookService.java.
- You won't see a visible change yet. You will verify both files compile correctly in the next substep.
Red underlines on Optional or List?
Check that your import statements at the top of the file match exactly: java.util.List and java.util.Optional. IntelliJ may suggest auto-importing them. Press Option+Enter (macOS) or Alt+Enter (Windows) on the red text to accept the import suggestion.
help me fix import errors in my BookService class
✔️ Awesome, I've got everything!
Great. Make sure you have saved BookService.java before moving on.
ⓧ I'd like to double check the full code
package com.example.bookstore;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
public Optional<Book> getBookById(Long id) {
return bookRepository.findById(id);
}
public Book createBook(Book book) {
return bookRepository.save(book);
}
public Optional<Book> updateBook(Long id, Book bookDetails) {
return bookRepository.findById(id).map(book -> {
book.setTitle(bookDetails.getTitle());
book.setAuthor(bookDetails.getAuthor());
book.setIsbn(bookDetails.getIsbn());
book.setPrice(bookDetails.getPrice());
return bookRepository.save(book);
});
}
public boolean deleteBook(Long id) {
if (bookRepository.existsById(id)) {
bookRepository.deleteById(id);
return true;
}
return false;
}
}
Verify compilation
Before moving on, confirm that both new files compile without errors. The Maven Wrapper included in your project handles this without needing a separate Maven installation.
- Compile the project by running this command in IntelliJ's terminal:
./mvnw compile
- You should see BUILD SUCCESS at the end of the output.
What does this command do?
./mvnw compile runs the Maven compile lifecycle phase using the Maven Wrapper bundled with your project. It compiles all Java source files in src/main/java and checks for syntax errors, missing imports, and type mismatches.
Seeing BUILD FAILURE?
- Read the error message carefully. It usually names the file and line number where the problem is.
- Check that BookRepository.java is in the same package as Book.java (com.example.bookstore).
- Confirm the package declaration at the top of each file says package com.example.bookstore;.
help me fix my Maven compile error
Your repository and service layer are wired up. Next up, you will create the REST controller that exposes these operations as HTTP endpoints you can call with curl.
Create the REST Controller
Your repository and service layer compiled cleanly in the last step. The data access and business logic layers are ready to go.
Now you need a way for the outside world to talk to your application. A REST controller is the front door of your API. It receives HTTP requests, delegates work to the service layer, and sends back JSON responses with the appropriate status codes.
In this step, get ready to:
- Create the BookController class with REST annotations.
- Implement five CRUD endpoints for your bookstore API.
- Test every endpoint with curl and verify correct responses.
Create the BookController class
The controller is the top layer of your architecture. It maps incoming HTTP requests to the service methods you already wrote.
- In IntelliJ, right-click the com.example.bookstore package in src/main/java.
- Select New then Java Class.
- Name it BookController and press Enter.
- Replace the entire file contents with this code:
package com.example.bookstore;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
}
What does this code do?
@RestController tells Spring Boot that this class handles HTTP requests and returns JSON automatically. Every method in this class becomes an API endpoint.
@RequestMapping("/api/books") sets the base URL path for all endpoints in this controller. Every endpoint you add will start with /api/books.
The constructor injects BookService using dependency injection. Spring finds the @Service-annotated class and provides it automatically.
- Save BookController.java.
Why no visible change yet?
The class compiles, but has no endpoint methods. You will add those next and then test everything together.
Implement the CRUD endpoints
Each endpoint maps an HTTP verb (GET, POST, PUT, DELETE) to a service method. Spring converts the return value to JSON and sets the HTTP status code you specify.
- In BookController.java, place your cursor inside the class body, after the constructor's closing brace.
- Add these five endpoint methods:
@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 ResponseEntity<Book> createBook(@RequestBody Book book) {
Book created = bookService.createBook(book);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book bookDetails) {
return bookService.updateBook(id, bookDetails)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
if (bookService.deleteBook(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
What does each endpoint do?
- getAllBooks() handles GET /api/books and returns every book in the database as a JSON array.
- getBookById() handles GET /api/books/{id}. It returns the book if found, or a 404 status if not.
- createBook() handles POST /api/books. It reads the JSON body with @RequestBody, saves the book, and returns 201 Created.
- updateBook() handles PUT /api/books/{id}. It updates the existing book's fields or returns 404 if the book does not exist.
- deleteBook() handles DELETE /api/books/{id}. It removes the book and returns 204 No Content, or 404 if not found.
- Save BookController.java.
- Verify compilation by running this in your terminal:
./mvnw compile
You should see BUILD SUCCESS in the output.
Seeing compilation errors?
- Check that the five methods are placed inside the BookController class body, between the constructor and the final closing brace.
- Make sure you have all the import statements at the top of the file. Missing imports for ResponseEntity or HttpStatus will cause errors.
- Confirm that BookService.java exists in the same package. The controller depends on it.
Still stuck? Help me fix compilation errors in my BookController
✔️ Awesome, I've got everything!
Great. Double check you have saved BookController.java before moving on.
ⓧ I'd like to double check the full code
package com.example.bookstore;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.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 ResponseEntity<Book> createBook(@RequestBody Book book) {
Book created = bookService.createBook(book);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book bookDetails) {
return bookService.updateBook(id, bookDetails)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
if (bookService.deleteBook(id)) {
return ResponseEntity.noContent().build();
}
return ResponseEntity.notFound().build();
}
}
Test the API with curl
Your controller is compiled and ready. Time to start the server and send real HTTP requests to prove everything works end to end.
- Start the application by running:
./mvnw spring-boot:run
Wait until you see a line containing Started BookstoreApplication in the output. Your server is now listening on port 8080.
Keep this terminal running
Open a second terminal tab or window for the curl commands. The server must stay running while you test.
- Create a book by running this command in your second terminal:
curl -X POST http://localhost:8080/api/books -H "Content-Type: application/json" -d '{"title":"The Great Gatsby","author":"F. Scott Fitzgerald","isbn":"978-0743273565","price":10.99}'
You should see a JSON response with the book data and an auto-generated id field. The HTTP status is 201 Created.
Windows PowerShell users
PowerShell treats single quotes differently. Use curl.exe instead of curl, and escape inner double quotes with backslashes. Alternatively, run the commands in Git Bash which handles bash-style quoting.
- Retrieve all books by running:
curl http://localhost:8080/api/books
You should see a JSON array containing the book you just created.
- Retrieve a single book by its ID:
curl http://localhost:8080/api/books/1
You should see the JSON object for the book with id value 1.
- Update the book's price by running:
curl -X PUT http://localhost:8080/api/books/1 -H "Content-Type: application/json" -d '{"title":"The Great Gatsby","author":"F. Scott Fitzgerald","isbn":"978-0743273565","price":12.99}'
You should see the updated book with price now showing 12.99.
- Delete the book by running:
curl -X DELETE http://localhost:8080/api/books/1
This returns an empty response with HTTP status 204 No Content.
- Confirm the book is gone by retrieving all books again:
curl http://localhost:8080/api/books
You should see an empty JSON array: [].
Getting connection refused?
- Make sure the server is still running in your first terminal. Look for Started BookstoreApplication in the output.
- Check that you are using port 8080 in the URL. If another application is using that port, stop it first.
- If you see a 404 error, verify that BookController.java is in the com.example.bookstore package. Spring only scans classes in the same package as the main application class.
Still stuck? Help me troubleshoot my Spring Boot REST API curl requests
- Stop the server by pressing Ctrl+C in the terminal where it is running.
Secret mission
Add Search by Author
Your bookstore API handles CRUD, but real users need to find books without knowing the exact ID. Can you add a search endpoint that filters books by author name, case-insensitively?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with an embedded in-memory database, so there are no ongoing costs.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The bookstore project folder (all Java source files, Maven wrapper, and configuration).
- JDK 21 (if installed during this project).
Keep everything running
No action needed. Choose this if you plan to continue building on this project or want to reference the code later.
- Your bookstore folder stays on your machine, ready to open in IntelliJ IDEA whenever you want.
- JDK 21 remains installed and available for other Java projects.
- The H2 database is in-memory, so no data persists between runs. There is nothing accumulating on disk.
Pause - I'll come back to this later
Make sure the Spring Boot server is stopped to free up port 8080. Since the database is in-memory, all book data is cleared automatically when the server stops.
- If the server is still running in your terminal, press Ctrl+C to stop it.
- Close the project in IntelliJ IDEA if you want to free up memory.
- Your project files remain intact. Re-open the bookstore folder in IntelliJ IDEA and run ./mvnw spring-boot:run to pick up where you left off.
Delete - I don't want to use this again
Remove all project files and optionally uninstall JDK 21 if you no longer need it for other Java work.
- Delete the bookstore project folder by running the appropriate command for your system.
macOS
Navigate to the parent folder where you extracted the project, then remove it:
rm -rf bookstore
Windows
Navigate to the parent folder where you extracted the project, then remove it:
rmdir /s /q bookstore
- Optionally uninstall JDK 21 if you do not plan to use Java for other projects.
macOS
If you installed via Homebrew, remove it with:
brew uninstall openjdk@21
Windows
- Open Settings.
- Select Apps, then Installed apps.
- Search for your JDK 21 installation, click the three-dot menu, and select Uninstall.
Nice Work!
Nice Work!
Nice work! You just built a fully functional REST API with Spring Boot that handles real CRUD operations against a database.
You've learned how to:
- Build a complete REST API with Spring Boot 4.0.7 using layered architecture (controller, service, repository).
- Create a JPA entity mapped to an embedded H2 database and implement a repository with Spring Data JPA that requires zero SQL.
- Test all five CRUD endpoints with curl and verify correct HTTP status codes and JSON responses.
- Secret Mission: Added a search-by-author endpoint using Spring Data JPA query derivation.
Ready to quiz yourself?