Build a Book Manager REST API
Build a Spring Boot REST API with full CRUD operations and H2 database.
Introduction
30 Second Summary
Every app you use daily has a backend that stores and retrieves data when you tap a button. Whether you search for a product, save a bookmark, or update your profile, a REST API is handling that request behind the scenes.
In this project, you will build a Book Manager REST API using Spring Boot. You will create endpoints that let you add, view, update, and delete books through HTTP requests, all backed by a real database.
What You'll Build
You will have a live API running on your machine that responds to requests in your browser and terminal, proving you can build the same backend patterns used in professional Java applications.
By the end of this project, you'll have:
- A running REST API where you send a GET request to /api/books and see a JSON list of all your books appear instantly.
- Full CRUD capabilities where you can create a book with a title and author, update its details, and delete it entirely, all through HTTP requests.
- A browser-accessible H2 database console where you can see your books stored in real database tables and run SQL queries against them.
- Secret Mission: Add a search endpoint that finds books by author name, using custom query methods that Spring generates for you automatically.
Are there any prerequisites?
None. This guide walks you through installing everything you need, including JDK 17 and generating your project from scratch. You just need a Windows machine with IntelliJ IDEA installed.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters. This gives every step ahead a clear purpose.
Install JDK 17 and Generate Your Spring Boot Project
Spring Boot needs Java to compile and run your code. Before you can write a single line, you need the Java Development Kit (JDK) installed on your machine.
You also need a project structure with the right dependencies already configured. Spring Initializr generates this for you in seconds, so you skip all manual configuration and jump straight into coding.
In this step, get ready to:
- Install JDK 17 and verify it works.
- Generate a Spring Boot project with the correct dependencies.
- Open the project in IntelliJ IDEA and confirm the structure.
Install JDK 17
Spring Boot 4.1.0 requires a minimum of Java 17. You will install the Eclipse Temurin distribution of JDK 17, which is free and open source.
- Open your browser and navigate to the Adoptium releases page:
- https://adoptium.net/temurin/releases
- Click JDK 17 - LTS in the version list.
- Select the Windows .msi installer for your system (x64) and download it.
- Run the downloaded .msi file to launch the installer.
- On the Custom Setup screen, make sure all checkboxes are enabled, including Set JAVA_HOME variable and Add to PATH. Click through to complete the installation.
Why JDK 17 specifically?
JDK 17 is a Long Term Support (LTS) release and the minimum version required by Spring Boot 4.1.0. Using the exact minimum keeps things simple and avoids compatibility surprises.
Now verify Java is installed and available in your terminal.
- Open a new Command Prompt or PowerShell window (important: open a new one so it picks up the updated PATH).
- Check your Java version by running this command:
java -version
What do you expect to see? Before reading on, predict whether this command will print a version number or an error.
✔️ I see version 17 or higher
Your output starts with something like openjdk version "17. followed by a build number. Java is ready to go.
ⓧ I see a version lower than 17
You have an older Java installed. You need JDK 17 for Spring Boot 4.1.0 to work correctly.
- Go to https://adoptium.net/temurin/releases and download the JDK 17 - LTS .msi installer for Windows x64.
- Run the installer. On the Custom Setup screen, enable the Set JAVA_HOME variable option. This ensures your system points to the new version.
- Close your current terminal and open a new one.
- Run java -version again. You should now see version 17.
ⓧ Java is not installed
Your system does not recognize the java command. Follow these steps to install JDK 17.
- Go to https://adoptium.net/temurin/releases and click JDK 17 - LTS.
- Download the Windows .msi installer (x64).
- Run the installer. On the Custom Setup screen, make sure Add to PATH and Set JAVA_HOME variable are both enabled.
- Complete the installation wizard.
- Close your terminal and open a fresh one.
- Run java -version again. You should now see output starting with openjdk version "17.
Still not seeing version 17?
If java -version still shows an old version after installing, close ALL open terminal windows and open a completely new one. The PATH update only takes effect in new sessions.
If you have multiple Java versions and the old one persists, check that the Temurin installer set JAVA_HOME. You can verify by running echo %JAVA_HOME% in Command Prompt. It should point to your Temurin 17 installation folder.
help me troubleshoot my Java installation
Generate your Spring Boot project
Spring Initializr is a web tool that generates a ready-to-go project with all your dependencies pre-configured. You will use it to create a Maven project with everything your Book Manager API needs.
- Open https://start.spring.io in your browser.
- Set Project to Maven.
- Set Language to Java.
- Set Spring Boot to the latest 4.x version available.
- Under Project Metadata, set Group to com.example.
- Set Artifact to bookmanager.
- Click Dependencies (or the ADD DEPENDENCIES button) and search for each of these one at a time:
- Spring Web (provides the web framework for your REST API).
- Spring Data JPA (connects your app to a database with minimal code).
- H2 Database (a lightweight in-memory database for development).
- Click Generate to download the project as a ZIP file.
What did Spring Initializr just do?
It generated a complete Maven project with a pom.xml that lists your three dependencies, a main application class, and configuration files. It also includes mvnw.cmd (the Maven Wrapper), which means you do not need to install Maven separately.
You should now have a file called bookmanager.zip in your Downloads folder.
Open the project in IntelliJ IDEA
Now you will unzip the generated project and open it in IntelliJ IDEA so that Maven can download all the dependencies.
- Open File Explorer and navigate to your Downloads folder.
- Right-click bookmanager.zip and select Extract All.
- Choose your Desktop as the destination and click Extract. This creates a bookmanager folder on your Desktop.
- Switch to IntelliJ IDEA.
- Click Open (or File then Open if you already have another project open).
- Navigate to your Desktop and select the bookmanager folder. Click OK.
- If IntelliJ asks whether to trust the project, click Trust Project.
What happens when IntelliJ opens the project?
IntelliJ detects the pom.xml file and recognizes this as a Maven project. It automatically downloads all the dependencies (Spring Web, Spring Data JPA, H2) from the internet. This process is called indexing and may take a minute or two.
You will see a progress bar at the bottom of IntelliJ while it works. Wait for it to finish before continuing.
Before you move on, predict what file you expect to see as the main entry point of a Spring Boot application.
- In the Project panel on the left side of IntelliJ, expand the folders: src then main then java then com.example.bookmanager.
- Confirm you see BookmanagerApplication.java. This is your application's entry point.
You should see BookmanagerApplication.java listed in the package. This file contains the main method that starts your entire Spring Boot application.
Don't see the file or folders?
If the Project panel is not visible, press Alt+1 to toggle it open.
If the folders appear empty or dependencies show errors, IntelliJ may still be indexing. Check for a progress bar at the bottom of the window. Wait for it to complete, then try expanding the folders again.
If you accidentally opened the wrong folder (the outer ZIP folder instead of bookmanager), close the project and re-open the correct bookmanager folder that contains the pom.xml file.
help me fix my IntelliJ IDEA project import
Your environment is ready. Next up, you will run this application and see your first REST endpoint respond in the browser.
Run Your First Endpoint
Your development environment is set up and your Spring Boot project is ready to go. Now it's time to see it actually do something.
Right now your application is just an empty shell that doesn't respond to any requests. In this step, you'll write your first REST endpoint and watch Spring Boot serve a response in your browser.
In this step, get ready to:
- Create a REST controller with a GET endpoint.
- Run the application and verify it responds to HTTP requests.
Create your first REST controller
A controller is the class that receives incoming HTTP requests and decides what to send back. Spring Boot uses annotations on the class and its methods to wire everything up automatically.
You'll create two files: a placeholder Book class (which you'll flesh out later) and the BookController that handles requests.
- In IntelliJ IDEA's Project tool window on the left, expand src > main > java > com.example.bookmanager.
- Right-click the bookmanager package and select New then Java Class.
- Type Book as the class name and press Enter.
IntelliJ generates the file with a package declaration and empty class body. Replace the entire contents of Book.java with this code:
package com.example.bookmanager;
public class Book {
}
Why create an empty Book class?
Your controller will return a List<Book> type. Java needs the Book class to exist for the code to compile, even though the list is empty for now.
You'll add fields like title and author to this class in the next step.
- Right-click the bookmanager package again and select New then Java Class.
- Type BookController as the class name and press Enter.
- Replace the entire contents of BookController.java with the following code:
package com.example.bookmanager;
import org.springframework.web.bind.annotation.*;
import java.util.List;
// Marks this class as a REST API controller
@RestController
// All endpoints in this controller start with /api/books
@RequestMapping("/api/books")
public class BookController {
// Handles GET requests to /api/books
@GetMapping
public List<Book> getAllBooks() {
return List.of();
}
}
What does this code do?
@RestController tells Spring Boot this class handles HTTP requests and returns data directly (not a web page).
@RequestMapping("/api/books") sets the base URL path for every endpoint in this controller.
@GetMapping maps HTTP GET requests to the getAllBooks() method. Right now it returns an empty list, which Spring Boot automatically converts to a JSON array [].
- Save both files by pressing Ctrl+S.
Seeing red underlines in BookController.java?
If you see red underlines under the import statements, IntelliJ may still be downloading dependencies. Check the bottom status bar for a progress indicator. Wait for it to finish, then the errors should disappear.
If the errors persist, try right-clicking your pom.xml file and selecting Maven then Reload Project.
help me fix red underlines in my Spring Boot controller imports
Run the application and test the endpoint
Your controller is ready. Time to start the application and hit your endpoint in the browser.
- Open BookmanagerApplication.java in the editor (it's in the same com.example.bookmanager package).
- Click the green play button in the gutter next to public static void main.
- Select Run 'BookmanagerApplication.main()' from the popup.
What happens when you click Run?
Spring Boot starts an embedded Tomcat web server inside your application. You don't need to install or configure a separate server. The console at the bottom of IntelliJ shows startup logs ending with a line like Started BookmanagerApplication.
- Wait for the console to show Started BookmanagerApplication (this takes a few seconds on first run).
Before you open the URL, what do you think you'll see at http://localhost:8080/api/books? An error page, a greeting, or something else?
- Open your browser and navigate to http://localhost:8080/api/books.
You should see [] displayed in the browser. That's an empty JSON array, which is exactly what your getAllBooks() method returns.
Don't see [] in the browser?
If you see a "Whitelabel Error Page" or the page won't load, check these common causes:
- Make sure the console shows Started BookmanagerApplication without any red error text above it.
- Confirm the URL is exactly http://localhost:8080/api/books with no typos.
- If you see a port conflict error in the console, another application is already using port 8080. Close it or stop the other process, then re-run.
help me troubleshoot my Spring Boot app not responding at localhost:8080
✔️ Awesome, I've got everything!
Great work. Make sure you've saved both Book.java and BookController.java.
ⓧ I'd like to double check the full code
Here's the complete BookController.java file:
package com.example.bookmanager;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
@GetMapping
public List<Book> getAllBooks() {
return List.of();
}
}
Your first endpoint is live and responding. Next up, you'll give the Book class real fields and return actual data from your API.
Create the Book Model and Return Hardcoded Data
Your API is running, but visiting /api/books returns an empty array. Before you can return meaningful data, you need to define what a "book" actually looks like in your system.
In this step, you will create a Book class as your data model. Then you will wire up hardcoded book objects so you can confirm the full request-to-JSON-response cycle works before adding database complexity later.
In this step, get ready to:
- Create a Book data model class with fields, constructors, and accessors.
- Return hardcoded book data from the API.
- Add an endpoint to retrieve a single book by its ID.
Create the Book data model class
A data model defines the shape of data your API works with. Your Book class will hold an ID, a title, and an author for each book.
- In IntelliJ's Project panel on the left, expand src/main/java/com/example/bookmanager.
- Right-click the bookmanager package folder.
- Select New, then Java Class.
- Type Book as the class name and press Enter.
- Replace the entire file contents with the following code that defines the fields and constructors:
package com.example.bookmanager;
public class Book {
// Each book has an ID, title, and author
private Long id;
private String title;
private String author;
// No-argument constructor (required by frameworks later)
public Book() {
}
// Constructor for creating a book with title and author
public Book(String title, String author) {
this.title = title;
this.author = author;
}
What does this code do?
You declared three private fields that represent a book's attributes. The id will be assigned programmatically, while title and author come from the constructor.
The no-argument constructor is empty for now, but Java frameworks often require it to create objects behind the scenes. The second constructor lets you create a book with its essential data in one line.
- Add the getters and setters below the constructors, still inside the Book class:
// Getter and setter for id
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
// Getter and setter for title
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
// Getter and setter for author
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
Why do I need getters and setters?
Getters and setters let other classes read and modify your book's fields in a controlled way. When Spring Boot converts your Book object to JSON, it uses these getter methods to read each field's value.
- Save Book.java by pressing Ctrl+S.
- Switch to BookController.java in the editor. The red underline on Book should disappear now that the class exists.
Still seeing a red underline on Book?
- Make sure Book.java is in the exact same package folder as BookController.java: src/main/java/com/example/bookmanager/.
- Confirm the first line of Book.java says package com.example.bookmanager; and matches the controller's package declaration.
Help me fix the red underline error in IntelliJ
✔️ Awesome, I've got everything!
Great. Make sure you saved Book.java before moving on.
⨯ I'd like to double check the full code
Your Book.java file contains two sections you built above. Scroll up and confirm you have:
- The package declaration, three private fields, a no-arg constructor, and a two-argument constructor (first chunk).
- Six getter/setter methods (getId, setId, getTitle, setTitle, getAuthor, setAuthor) followed by the closing } brace (second chunk).
Return hardcoded books from the controller
Now that the Book class exists, you can create book objects and return them from your API. Hardcoded data is perfect for testing the response shape without any database setup.
- Open BookController.java in the editor.
- Find the getAllBooks() method.
- Replace the entire method body with the following code:
@GetMapping
public List<Book> getAllBooks() {
// Create two hardcoded books for testing
Book b1 = new Book("The Great Gatsby", "F. Scott Fitzgerald");
b1.setId(1L);
Book b2 = new Book("To Kill a Mockingbird", "Harper Lee");
b2.setId(2L);
// Return both books as a list
return List.of(b1, b2);
}
What does this code do?
You create two Book objects using the constructor you just wrote, then manually set their IDs with setId(). The L suffix marks the number as a Long type, matching your field declaration.
Spring Boot automatically converts the returned List<Book> into a JSON array. Each book's getters determine which fields appear in the response.
- Save BookController.java by pressing Ctrl+S.
- Stop the running application by clicking the red square Stop button in IntelliJ's toolbar at the top right.
- Restart the application by clicking the green Run button (play icon) next to the main method in BookmanagerApplication.java.
Before you check the result, what do you think the response will look like at http://localhost:8080/api/books?
- Open your browser and navigate to http://localhost:8080/api/books.
You should see a JSON array containing two book objects, each with id, title, and author fields.
Seeing an empty array or an error page?
- Make sure you stopped and restarted the application after saving your changes.
- Check that your getAllBooks() method returns List.of(b1, b2) and not the old List.of().
- Look at the IntelliJ console for red error messages. A compilation error means something in your code has a typo.
Help me debug why my /api/books endpoint is not returning book data
Add a get-by-id endpoint
A REST API typically lets you retrieve a single resource by its ID. You will add a second endpoint that accepts an ID in the URL path and returns just that book.
- In BookController.java, add the following method below the getAllBooks() method's closing brace:
@GetMapping("/{id}")
public Book getBookById(@PathVariable Long id) {
// Recreate the same hardcoded list
Book b1 = new Book("The Great Gatsby", "F. Scott Fitzgerald");
b1.setId(1L);
Book b2 = new Book("To Kill a Mockingbird", "Harper Lee");
b2.setId(2L);
// Search the list for a book matching the requested ID
return List.of(b1, b2).stream()
.filter(b -> b.getId().equals(id))
.findFirst()
.orElse(null);
}
What does this code do?
The /{id} in the annotation creates a URL path variable. When someone visits /api/books/1, Spring extracts 1 and passes it to the method as the id parameter.
The @PathVariable annotation tells Spring to pull the value from the URL. The stream filter searches the list for a matching ID and returns null if no book is found.
- Save BookController.java.
- Stop and restart the application using the Stop and Run buttons.
What do you expect to see when you visit /api/books/1 compared to /api/books?
- Open your browser to http://localhost:8080/api/books/1.
You should see a single JSON object with id of 1, the title "The Great Gatsby", and the author "F. Scott Fitzgerald".
Getting a 404 or a blank page?
- Confirm you restarted the app after adding the new method.
- Check that your @GetMapping("/{id}") annotation is inside the BookController class but outside the getAllBooks() method.
- Make sure the method parameter has the @PathVariable annotation. Without it, Spring does not know to extract the ID from the URL.
Help me fix my get-by-id endpoint
✔️ Awesome, I've got everything!
Great. Make sure BookController.java is saved before moving on.
⨯ I'd like to double check the full code
Here is what your BookController.java should look like now:
package com.example.bookmanager;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
@GetMapping
public List<Book> getAllBooks() {
Book b1 = new Book("The Great Gatsby", "F. Scott Fitzgerald");
b1.setId(1L);
Book b2 = new Book("To Kill a Mockingbird", "Harper Lee");
b2.setId(2L);
return List.of(b1, b2);
}
@GetMapping("/{id}")
public Book getBookById(@PathVariable Long id) {
Book b1 = new Book("The Great Gatsby", "F. Scott Fitzgerald");
b1.setId(1L);
Book b2 = new Book("To Kill a Mockingbird", "Harper Lee");
b2.setId(2L);
return List.of(b1, b2).stream().filter(b -> b.getId().equals(id)).findFirst().orElse(null);
}
}
Your API now returns real book data and supports fetching individual books by ID. Next up, you will connect to a real database so your books persist across restarts instead of being hardcoded.
Connect to the H2 Database with Spring Data JPA
Your API currently returns hardcoded book objects that live inside the controller. Change that code and those books disappear forever.
To persist data properly, you need a real database. Spring Data JPA lets you connect your Book class to a database table with just a few annotations, and H2 runs as an in-memory database embedded directly in your application.
In this step, get ready to:
- Convert the Book class into a JPA entity mapped to a database table.
- Create a BookRepository interface for database access.
- Enable and test the H2 database console in your browser.
Convert Book into a JPA entity
A JPA entity is a Java class that maps directly to a database table. Each instance of the class becomes a row in that table.
- In IntelliJ IDEA, open Book.java.
- Find the top of the file starting at the package line through the field declarations.
- Replace everything from the package statement down to (and including) private String author; with the following code:
package com.example.bookmanager;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
// Marks this class as a database table
@Entity
public class Book {
// Primary key, auto-generated by the database
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String author;
What do these annotations do?
- @Entity tells Spring Data JPA to map this class to a database table called BOOK.
- @Id marks the id field as the table's primary key.
- @GeneratedValue(strategy = GenerationType.IDENTITY) tells the database to auto-increment the ID for each new row.
- The jakarta.persistence imports come from Jakarta EE, which Spring Boot 4.x uses for its persistence layer.
- Leave the rest of the file (constructors, getters, and setters) unchanged.
- Save Book.java.
Create the BookRepository interface
Spring Data JPA can generate all the database operations you need from a single interface. You declare the interface, and Spring writes the implementation at runtime.
- Right-click the com.example.bookmanager package in the Project sidebar (the same folder that contains Book.java).
- Select New, then Java Class.
- Name it BookRepository and select Interface as the kind.
- Replace the generated contents with the following code:
package com.example.bookmanager;
import org.springframework.data.jpa.repository.JpaRepository;
// Provides all CRUD operations for the Book entity
public interface BookRepository extends JpaRepository<Book, Long> {
}
What does this interface do?
By extending JpaRepository<Book, Long>, Spring Data JPA automatically gives you methods like findAll(), findById(), save(), and delete().
You wrote zero implementation code. Spring generates it all from the interface definition at startup.
- Save BookRepository.java.
- Stop the running application (click the red square stop button in IntelliJ) and restart it by clicking the green play button next to main in BookmanagerApplication.java.
The application starts successfully, which confirms your entity annotations and repository interface are correctly configured.
Seeing a compilation error on startup?
- Check that your imports use jakarta.persistence (not javax.persistence). Spring Boot 4.x requires the Jakarta namespace.
- Make sure Book.java still has the no-argument constructor (public Book() {}). JPA requires it to create instances from the database.
- Confirm that BookRepository is an interface, not a class.
Help me fix a JPA startup error in my Spring Boot project.
Configure the H2 database console
H2 includes a browser-based console that lets you run SQL queries against your database. You need to enable it in your application configuration.
- In IntelliJ IDEA, open src/main/resources/application.properties (this file already exists but is empty).
- Add the following configuration:
# Enable the H2 browser console at /h2-console
spring.h2.console.enabled=true
# Tell Hibernate to update the schema to match your entities
spring.jpa.hibernate.ddl-auto=update
# Print SQL statements to the console for debugging
spring.jpa.show-sql=true
What do these properties do?
- spring.h2.console.enabled=true turns on the H2 web console at /h2-console.
- spring.jpa.hibernate.ddl-auto=update tells Hibernate to create or update database tables to match your entity classes.
- spring.jpa.show-sql=true prints every SQL statement Hibernate runs, so you can see the table creation in your console output.
- Save application.properties.
- Stop and restart the application.
Before you open the H2 console, look at your startup logs in the IntelliJ Run panel. You should see a line containing the database JDBC URL. It will start with jdbc:h2:mem: followed by an auto-generated name. Copy this entire URL.
What do you think will happen when you run SELECT * FROM BOOK in the console? Will there be any rows?
- Open your browser and navigate to http://localhost:8080/h2-console.
- In the JDBC URL field, paste the URL you copied from the startup logs (it starts with jdbc:h2:mem:).
- Set User Name to sa.
- Leave the Password field empty.
- Click Connect.
- In the SQL input area, type SELECT * FROM BOOK and click Run.
You should see the BOOK table with columns ID, TITLE, and AUTHOR, with zero rows. The table is empty because you have not created any books through the API yet. The important thing is that the table exists, which proves your JPA entity is correctly mapped to the database.
Can't connect to the H2 console?
- Make sure you copied the full JDBC URL from the startup logs, including the auto-generated name after jdbc:h2:mem:. The default URL shown in the H2 console form will not work.
- Double-check that spring.h2.console.enabled is spelled correctly in application.properties. A common mistake is typing enable instead of enabled.
- Confirm the app is still running on port 8080.
Help me connect to the H2 database console in my Spring Boot project.
✔️ Awesome, I've got everything!
Great. Make sure you have saved all three files before moving on.
ⓧ I'd like to double check the full code
Here is Book.java with the JPA annotations:
package com.example.bookmanager;
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;
public Book() {
}
public Book(String title, String author) {
this.title = title;
this.author = author;
}
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;
}
}
Here is BookRepository.java:
package com.example.bookmanager;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
}
Here is application.properties:
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
Your Book class is now a real database entity, and you have a repository ready to perform any database operation. Next up, you will build the full CRUD API with a service layer that connects your controller to this repository.
Build the Full CRUD API with the Service Layer
Your controller currently returns fake, hardcoded book data that never changes. In a real application, the controller should focus on handling HTTP requests while a separate service layer manages all the business logic.
The service layer sits between the controller and the repository. It keeps your code organized and testable by giving each layer one clear job. In this step, you will create a BookService and wire it into your controller so every request flows through real database operations.
In this step, get ready to:
- Create a BookService class with full CRUD methods.
- Refactor BookController to delegate to the service layer.
- Test all endpoints (POST, GET, PUT, DELETE) with curl.
Create the BookService class
The service class holds your application's business logic. It uses the BookRepository you created earlier to talk to the database.
- In IntelliJ IDEA, right-click the com.example.bookmanager package in the Project sidebar, then select New > Java Class.
- Name the class BookService and press Enter.
- Replace the generated contents with this code:
package com.example.bookmanager;
import org.springframework.stereotype.Service;
import java.util.List;
// Marks this class as a Spring-managed service component
@Service
public class BookService {
// The repository that handles all database operations
private final BookRepository bookRepository;
// Spring injects the repository automatically via constructor
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
// Returns every book stored in the database
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
// Finds one book by ID, throws an error if not found
public Book getBookById(Long id) {
return bookRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Book not found with id: " + id));
}
}
What does this code do?
The @Service annotation tells Spring to manage this class and make it available for injection into other components.
- Constructor injection means Spring automatically passes the BookRepository when it creates a BookService instance.
- The findAll() method comes free from JpaRepository. You never wrote it yourself.
- The orElseThrow() call means your API returns a clear error instead of silently returning null when a book ID does not exist.
- Save BookService.java with Ctrl+S.
- Confirm IntelliJ shows no red underlines in the file. The class compiles cleanly.
Now add the write methods that handle creating, updating, and deleting books. These go inside the BookService class, below the getBookById method.
- In BookService.java, place your cursor after the closing } of the getBookById method (but before the final } of the class).
- Add these three methods:
// Saves a new book to the database
public Book createBook(Book book) {
return bookRepository.save(book);
}
// Updates an existing book's title and author
public Book updateBook(Long id, Book bookDetails) {
Book book = getBookById(id);
book.setTitle(bookDetails.getTitle());
book.setAuthor(bookDetails.getAuthor());
return bookRepository.save(book);
}
// Deletes a book by ID after confirming it exists
public void deleteBook(Long id) {
Book book = getBookById(id);
bookRepository.delete(book);
}
What do these methods do?
- The createBook method calls save() on the repository. Spring Data JPA handles the INSERT SQL for you.
- The updateBook method first fetches the existing book, changes its fields, then saves it again. This triggers an UPDATE instead of an INSERT.
- The deleteBook method fetches the book first so it throws a clear error if the ID does not exist, then deletes it.
- Save BookService.java.
- Confirm there are no red underlines anywhere in the file. Your service is ready.
Seeing red underlines in BookService?
- Check that your import section includes import org.springframework.stereotype.Service; and import java.util.List;.
- Make sure BookRepository.java exists in the same package. The service depends on it.
- If IntelliJ shows "Cannot resolve symbol," try clicking File > Invalidate Caches / Restart.
help me fix red underlines in my BookService class
✔️ Awesome, I've got everything!
Great. Double check you have saved BookService.java and the file shows no errors.
ⓧ I'd like to double check the full code
Scroll up and confirm your BookService.java contains these sections in order:
- Package declaration and imports (Service and List).
- The @Service annotation and class declaration.
- A private final BookRepository bookRepository field with a constructor that accepts it.
- Five methods: getAllBooks, getBookById, createBook, updateBook, deleteBook.
Refactor BookController to use the service
Your controller still returns hardcoded books. Now you will replace its entire contents so every request flows through the service layer to the database.
- Open BookController.java in IntelliJ.
- Select all the code with Ctrl+A and delete it.
- Paste in this replacement code:
package com.example.bookmanager;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
// Spring injects BookService automatically via constructor
public BookController(BookService bookService) {
this.bookService = bookService;
}
// Delegates to the service instead of returning hardcoded data
@GetMapping
public List<Book> getAllBooks() {
return bookService.getAllBooks();
}
@GetMapping("/{id}")
public Book getBookById(@PathVariable Long id) {
return bookService.getBookById(id);
}
}
What changed?
The hardcoded Book objects are gone. Instead, the controller declares a BookService field and receives it through constructor injection. Each GET method now calls the service, which calls the repository, which talks to the database.
Now add the remaining endpoints for creating, updating, and deleting books. Place these methods inside the class, below the getBookById method but above the final }.
- Add these methods at the bottom of the BookController class body:
// POST /api/books - creates a new book, returns 201 Created
@PostMapping
public ResponseEntity<Book> createBook(@RequestBody Book book) {
Book created = bookService.createBook(book);
return new ResponseEntity<>(created, HttpStatus.CREATED);
}
// PUT /api/books/{id} - updates an existing book
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book book) {
return bookService.updateBook(id, book);
}
// DELETE /api/books/{id} - removes a book, returns 204 No Content
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
return ResponseEntity.noContent().build();
}
What do these annotations do?
- The @PostMapping maps POST requests to the createBook method. @RequestBody tells Spring to parse the incoming JSON into a Book object.
- The ResponseEntity wrapper lets you control the HTTP status code. HttpStatus.CREATED returns 201 instead of the default 200.
- The @DeleteMapping method returns ResponseEntity.noContent().build() which sends a 204 status with an empty body.
- Save BookController.java with Ctrl+S.
- Confirm IntelliJ shows no red underlines in the file.
- Stop the running application by clicking the red square Stop button in the top toolbar.
- Restart the application by clicking the green Run button next to the main method in BookmanagerApplication.java.
- Open http://localhost:8080/api/books in your browser. You should see [] because the database starts empty.
Application won't start?
- If you see a "Could not autowire" error, confirm BookService.java has the @Service annotation. Without it, Spring cannot find the class.
- If you see a syntax error, check that the closing } of the class is present after all your methods.
- Make sure you stopped the old instance before starting a new one. Two instances on the same port conflict.
help me fix my Spring Boot application startup error
✔️ Awesome, I've got everything!
Great. Double check you have saved BookController.java and the app is running with no errors in the console.
ⓧ I'd like to double check the full code
Scroll up and confirm your BookController.java contains these sections in order:
- Package declaration and imports (HttpStatus, ResponseEntity, web.bind.annotation.*, List).
- The @RestController and @RequestMapping("/api/books") annotations on the class.
- A private final BookService bookService field with a constructor.
- Five endpoint methods: getAllBooks (GET), getBookById (GET), createBook (POST), updateBook (PUT), deleteBook (DELETE).
Test all CRUD endpoints with curl
Your API is live and connected to a real database. Time to prove every operation works by sending requests from the command line.
Before you run the first command, what do you think the response will contain? The database is currently empty.
- Create a book by running this command in your terminal (Command Prompt or PowerShell):
curl -X POST http://localhost:8080/api/books -H "Content-Type: application/json" -d "{\"title\":\"The Great Gatsby\",\"author\":\"F. Scott Fitzgerald\"}"
You should see JSON returned with an id field (the database generated it), plus the title and author you sent.
- Retrieve all books to confirm it was saved:
curl http://localhost:8080/api/books
You should see a JSON array containing the book you just created.
- Update the book's title by running this PUT request:
curl -X PUT http://localhost:8080/api/books/1 -H "Content-Type: application/json" -d "{\"title\":\"Gatsby Updated\",\"author\":\"F. Scott Fitzgerald\"}"
The response shows the book with its title changed to Gatsby Updated.
- Delete the book by running:
curl -X DELETE http://localhost:8080/api/books/1
This returns an empty response with no body. That is the 204 No Content status.
- Confirm deletion by fetching all books one more time:
curl http://localhost:8080/api/books
You should see [] again. The book is gone.
What is the layered architecture?
Every request now flows through three layers: Controller (handles HTTP) > Service (business logic) > Repository (database access). This is the standard pattern in professional Spring Boot applications.
Each layer has one job. If you need to add validation before saving a book, it goes in the service. If you need to change the database, only the repository changes. The controller never touches SQL.
Curl returning an error?
- If you see "Connection refused," your application is not running. Click the green Run button in IntelliJ to start it.
- If you see a 500 error on PUT or DELETE, the book with that ID may not exist. Run the POST command again first to create a new book.
- On PowerShell, if the curl command fails, try wrapping the entire -d argument in single quotes instead of double quotes.
help me fix my curl request to the Spring Boot API
Secret mission
Add Search by Author
Your API handles basic CRUD, but real-world APIs need filtering and search. Can you add a custom query endpoint that finds books by author name using Spring Data JPA's query derivation?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with an in-memory database, so there are no ongoing costs.
Resources you used:
- The running Spring Boot application on localhost:8080.
- The bookmanager project folder.
- JDK 17 (Eclipse Temurin).
Keep everything running
No action needed. Choose this if you plan to keep building on your Book Manager API or want to experiment with more endpoints.
- Your Spring Boot application will keep running until you stop it or close IntelliJ IDEA.
- The H2 database is in-memory, so any books you created will disappear the next time you restart the application.
- JDK 17 stays installed on your system for future Java projects.
Pause - I'll come back to this later
Stop the running application to free up the port, but keep your project files for later.
- In IntelliJ IDEA, click the red square stop button in the top toolbar to stop the running application.
- Your project files in the bookmanager folder remain untouched. Open IntelliJ IDEA and select the folder again to resume.
- JDK 17 stays installed and ready for when you return.
Delete - I don't want to use this again
Remove all project files and optionally uninstall JDK 17.
- In IntelliJ IDEA, click the red square stop button to stop the running application. Then close the project.
- Delete the bookmanager folder. Open PowerShell and run the command below, replacing the path with where you unzipped the project.
Remove-Item -Recurse -Force ~\Desktop\bookmanager
- To uninstall JDK 17, open Settings, select Apps, then Installed apps. Search for Eclipse Temurin JDK, click the three-dot menu, and select Uninstall.
- If you plan to work on any other Java projects in the future, keep JDK 17 installed.
Nice Work!
Nice Work!
You just built a fully functional REST API from scratch using Spring Boot. Your Book Manager API handles real HTTP requests, persists data to a database, and follows the exact layered architecture pattern used in professional Java backends.
What you learned:
- Built a REST API with full CRUD operations (Create, Read, Update, Delete) using Spring Boot 4.1.0 and core annotations like @RestController, @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping.
- Connected your API to an H2 in-memory database using Spring Data JPA, with zero SQL code required.
- Implemented the Controller > Service > Repository layered architecture used in professional Java applications to keep your code organized and testable.
- Secret Mission: Added a custom search endpoint using Spring Data JPA's query derivation, proving you can extend beyond basic CRUD with methods like findByAuthorContainingIgnoreCase.
Ready to quiz yourself?